﻿using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using YWM.Entities;
using YWM.Repositories;

namespace YWM.EntityFrameworkCore
{
    public abstract class EfCoreRepositoryBase<TDbContext, TEntity, TKey> :
        RepositoryBase<TEntity, TKey>
        where TDbContext : EFCoreDbContext
        where TEntity : class, IEntityOfTKey<TKey>
    {
        private TDbContext _dbContext;
        protected TDbContext DbContext
        {
            get
            {
                return _dbContext;
            }
        }

        protected DbSet<TEntity> Table
        {
            get
            {
                return DbContext.Set<TEntity>();
            }
        }

        public EfCoreRepositoryBase()
        {
            _dbContext = YApp.Resolve<TDbContext>();
        }

        #region Select/Get/Query

        public override IQueryable<TEntity> GetAll()
        {
            return GetAllIncluding();
        }

        public override IQueryable<TEntity> GetAllIncluding(params Expression<Func<TEntity, object>>[] propertySelectors)
        {
            IQueryable<TEntity> query = Table.AsQueryable();

            if (propertySelectors != null && propertySelectors.Length > 0)
            {
                foreach (Expression<Func<TEntity, object>> propertySelector in propertySelectors)
                {
                    query = query.Include(propertySelector);
                }
            }

            return query;
        }

        public override Task<List<TEntity>> GetAllListAsync()
        {
            return GetAll().ToListAsync();
        }

        public override Task<List<TEntity>> GetAllListAsync(Expression<Func<TEntity, bool>> predicate)
        {
            return GetAll().Where(predicate).ToListAsync();
        }

        public override Task<TEntity> GetAsync(TKey id)
        {
            return FirstOrDefaultAsync(id);
        }

        public override Task<TEntity> SingleAsync(Expression<Func<TEntity, bool>> predicate)
        {
            return GetAll().SingleAsync(predicate);
        }

        public override Task<TEntity> FirstOrDefaultAsync(TKey id)
        {
            return FirstOrDefaultAsync(CreateEqualityExpressionForId(id));
        }

        public override Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate)
        {
            return GetAll().FirstOrDefaultAsync(predicate);
        }

        #endregion

        #region Insert

        public override TEntity Insert(TEntity entity)
        {
            return Table.Add(entity).Entity;
        }

        public override async Task<TEntity> InsertAsync(TEntity entity)
        {
            EntityEntry<TEntity> entityEntry = await Table.AddAsync(entity);
            return entityEntry.Entity;
        }

        public override TKey InsertAndGetId(TEntity entity)
        {
            entity = Insert(entity);

            if (entity.IsTransient())
            {
                DbContext.SaveChanges();
            }

            return entity.Id;
        }

        public override async Task<TKey> InsertAndGetIdAsync(TEntity entity)
        {
            entity = await InsertAsync(entity);

            if (entity.IsTransient())
            {
                await DbContext.SaveChangesAsync();
            }

            return entity.Id;
        }

        public override TKey InsertOrUpdateAndGetId(TEntity entity)
        {
            entity = InsertOrUpdate(entity);

            if (entity.IsTransient())
            {
                DbContext.SaveChanges();
            }

            return entity.Id;
        }

        public override async Task<TKey> InsertOrUpdateAndGetIdAsync(TEntity entity)
        {
            entity = await InsertOrUpdateAsync(entity);

            if (entity.IsTransient())
            {
                await DbContext.SaveChangesAsync();
            }

            return entity.Id;
        }

        #endregion

        #region Update

        public override TEntity Update(TEntity entity)
        {
            AttachIfNot(entity);
            DbContext.Entry(entity).State = EntityState.Modified;
            Table.Update(entity);
            return entity;
        }

        #endregion

        #region Delete

        public override void Delete(TEntity entity)
        {
            AttachIfNot(entity);
            Table.Remove(entity);
        }

        public override void Delete(TKey id)
        {
            TEntity entity = GetFromChangeTrackerOrNull(id);
            if (entity != null)
            {
                Delete(entity);
                return;
            }

            entity = FirstOrDefault(id);
            if (entity != null)
            {
                Delete(entity);
                return;
            }

            //Could not found the entity, do nothing.
        }

        #endregion

        #region Aggregates

        public override Task<int> CountAsync()
        {
            return GetAll().CountAsync();
        }

        public override Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate)
        {
            return GetAll().CountAsync(predicate);
        }

        public override Task<long> LongCountAsync()
        {
            return GetAll().LongCountAsync();
        }

        public override Task<long> LongCountAsync(Expression<Func<TEntity, bool>> predicate)
        {
            return GetAll().LongCountAsync(predicate);
        }

        #endregion

        protected virtual void AttachIfNot(TEntity entity)
        {
            EntityEntry entry = DbContext.ChangeTracker.Entries().FirstOrDefault(ent => ent.Entity == entity);
            if (entry != null)
            {
                return;
            }

            Table.Attach(entity);
        }

        private TEntity GetFromChangeTrackerOrNull(TKey id)
        {
            EntityEntry entry = DbContext.ChangeTracker.Entries()
                .FirstOrDefault(
                    ent =>
                        ent.Entity is TEntity &&
                        EqualityComparer<TKey>.Default.Equals(id, (ent.Entity as TEntity).Id)
                );

            return entry?.Entity as TEntity;
        }
    }
}